<?php namespace lfy;

class bin{

	//!	string short options for getopt(). 
	/*! in the shortopt string a::bcd:e -
		- a may optionally be passed with a corresponding value
		- b, c and e cannot have values: -a -b -c | -abc | -ab -c | etc
		- d must be passed with a value, or fatal error: -d value | -d=value | -dvalue
	*/

	public static $shortopts = "";

	//! array long options for getopt().
	/*! same rules as #shortopts, but in an array, e.g.
		\code array('regular-option', 'with-optional-value::', 'must-have-value:');
		\endcode
		also shortopt aliases can be assigned here
		\code array('a'=>'aliased-option');
		\endcode
	*/

	public static $longopts = array();
	
	// default option values
	
	public static $defaults = array();
	
	// holds our parsed options
	
	public static $options = array();
	
	public static $arguments = array();
	
	// whether to merge short and long opts with parent classes
	
	public static $merge = false;
	
	public static $usage = <<<USAGE
	
Usage instructions go in here.


USAGE;


	public static function initialize(){
		if(static::$merge) static::_merge_opts();
		static::_getopt();
	}

	//!	main functionality of program

	public static function main(){}
	
	
	protected static function _merge_opts(){
		$g = get_class();
		$c = get_called_class();
		$a = array();
		do{
			array_unshift($a, $c);
		}while($c::$merge && (($c = get_parent_class($c)) != $g));
		$short = '';
		$long = array();
		foreach($a as $c){
			$short .= $c::$shortopts;
			$long = array_merge($long, $c::$longopts);
		}
		static::$shortopts = $short;
		static::$longopts = array_unique($long);
	}

	//!	fetches argument by integer index
	//!	@return string

	public static function argument($i){
		return array_pick($i, static::$arguments);
	}
	
	// alias of above
	
	public static function arg($i){
		return array_pick($i, static::$arguments);
	}

	//!	fetches named option value.
	//! may be passed multiple names, e.g. option('f', 'file') whereby it will return the first one found
	//!	@return string

	public static function option($k){
		return array_pick($k, static::$options);
	}

	// alias

	public static function opt($k){
		return array_pick($k, static::$options);
	}
	
	
	protected static function _parseopts($short, array $long){
	
		$short_a = array();
	
		if(preg_match_all('/[a-zA-Z]::/', $short, $m)){
			foreach($m[0] as $k) $short_a[substr($k, 0, 1)] = 2;
		}
	
		if(preg_match_all('/[a-zA-Z]:/', $short, $m)){
			foreach($m[0] as $k){
				$k = substr($k, 0, 1);
				if(array_key_exists($k, $short_a)) continue;
				$short_a[$k] = 1;
			}
		}
	
		foreach(str_split($short) as $k){
			if(array_key_exists($k, $short_a)) continue;
			$short_a[$k] = 0;
		}	
	
		$long_a = array();

		foreach($long as $alias=>$k){
			$n = 0;
			while(substr($k, -1) == ':'){
				$k = substr($k, 0, -1);
				$n++;
			}
			$long_a[$k] = $n;
			if(!is_integer($alias)){
				if(isset($short_a[$alias])) 
					trigger_error("Cannot assign '$alias' to longopt alias; is already shortopt", E_USER_ERROR);
				$short_a[$alias] = $n;
			}
		}
		
		return array($short_a, $long_a);
	
	}
	

	//! checks option is expected and returns requirement integer

	protected static function _checkopt($k, $array){
		$x = array_pick($k, $array);
		if(is_null($x)){
			echo static::$usage;
			trigger_error("$k: invalid option", E_USER_ERROR);
		}
		return $x;
	}

	//!	Like PHP's getopt, but stricter and more informative.
	/*!	With PHP's getopt, using a single colon to insist on a value means 
		that if a user passes the option but \a omits that value then the option is 
		simply ignored.
		Here, unrecognised options and empty options that require values raise errors, 
		and a list of arguments is populated.
	*/

	protected static function _getopt(){

		// PHP's argv is an excellent parse of the command string
		$argv = $GLOBALS['argv'];
		
		// get our options and what is expected of them
		list($short_a, $long_a) = self::_parseopts(static::$shortopts, static::$longopts);
		
		$arguments = array();
		$short = array();
		$long = array();
		
		define('BIN_SCRIPT', array_shift($argv));
		define('BIN_CLASS', get_called_class());
		define('BIN_PWD', exec('pwd'));
		
		for(reset($argv); $opt = current($argv); next($argv)){
	
			if(substr($opt, 0, 1) != '-'){ // argument
				$arguments[] = $opt;
				continue;
			}

			$opt = substr($opt, 1);
			$k = substr($opt, 0, 1);

			if($k != '-'){ // short opt
			
				if($x = static::_checkopt($k, $short_a)){ // not 0, so can be assigned value

					if(substr($opt, 1, 1) == '='){ // asssignment -k=value
						$short[$k] = substr($opt, 2);
						continue;
					}

					if($rest = substr($opt, 1)){ // rest is value -kvalue
						$short[$k] = $rest;
						continue;
					}
				
					if($x == 2){ // value is optional, and there isn't one. assign int
						if(!isset($short[$k])) $short[$k] = 1;
						else{
							$short[$k] = (int) $short[$k];
							$short[$k]++;
						}
						continue;							
					}
				
					// value required; is next opt in list
					next($argv);
					if(is_null(key($argv))){
						static::usage();
						trigger_error("Option '$k' requires a value.", E_USER_ERROR);
					}
					$short[$k] = current($argv);
					continue;
				}
			
				// string of short opts with no value -abc
				foreach(str_split($opt) as $k){
					$x = self::_checkopt($k, $short_a);
					if($x == 1){ // no burying required values in string of short opts
						static::usage();
						trigger_error("Option '$k' requires a value.", E_USER_ERROR);
					}
					if(!isset($short[$k])) $short[$k] = 1;
					else{
						$short[$k] = (int) $short[$k];
						$short[$k]++;
					}
				}
			
				continue;
			}

			// long opts
			$opt = substr($opt, 1);

			// check for assignment
			$e = explode('=', $opt, 2);
			$k = array_shift($e);
			$v = count($e) ? array_shift($e) : null;
		
			if($x = self::_checkopt($k, $long_a)){ // not 0, can be assigned value
		
				if(!is_null($v)){ // assignment --key=value
					$long[$k] = $v;
					continue;
				}

				if($x == 2){ // value is optional, and absent
					$long[$k] = true;
					continue;						
				}
			
				// value required; is next opt
				next($argv);
				if(is_null(key($argv))){
					static::usage();
					trigger_error("Option '$k' requires a value.", E_USER_ERROR);
				}
				$long[$k] = current($argv);
				continue;
			}

			// no value required, simply set true
			$long[$k] = true;

		} //end loop

	
		$defaults = static::_normopts(static::$defaults);
		$options  = static::_normopts(array_merge($short, $long));
		$options = array_merge($defaults, $options);
		
		static::$options = $options;
		static::$arguments = $arguments;
	
	}


	protected static function _normopts($options){
		foreach(static::$longopts as $alias=>$k){
			if(!is_integer($alias)){
				while(substr($k, -1) == ':')
					$k = substr($k, 0, -1);
				if(isset($options[$k]) && isset($options[$alias]))
					trigger_error("Can't set long option '$k' and alias '$alias' simultaneously", E_USER_ERROR);
				if(isset($options[$k]))
					$options[$alias] = $options[$k];
				else if(isset($options[$alias]))
					$options[$k] = $options[$alias];
			}
		}
		return $options;
	}


	/*!	ask the user a question.

		\param question
		\param responses :
			-	null: y/n required, returns bool
			-	bool: y/n default bool on empty response, returns bool
			-	string: default string on empty response, returns string
			-	array: indexed list of responses, returns integer index
			-	function: user function that takes 2 arguments; ($answer, &$question) 
			user function should return bool success and set question by reference if false
		\code
				
		\endcode	
		\param bool echo_off
	*/		

	public function ask($question, $responses=null, $echo_off=false){

		switch(true){
	
			case is_null($responses):
		
				echo "$question [y/n]: ";
				break;
			
			case is_bool($responses):
		
				$def = $responses ? 'y' : 'n';
				echo "$question [y/n] ($def): ";
				break;

			case is_string($responses):

				echo "$question [$responses]: ";
				break;

			case is_array($responses):

				echo "$question\n";
				$i = 0;
				foreach($responses as $k=>$v){
					$key = is_integer($k) ? '' : "$k => ";
					echo "\t[$i] $key$v\n";
					$i++;
				}
				reset($responses);
				echo "Enter your choice [0]: ";
				break;

			case is_callable($responses):

				echo "$question: ";
				break;

			default: 
		
				back_error("Second parameter to lfy\bin::ask must be null, string, bool, array or function", E_USER_ERROR);
		}

		$answer = static::get_answer($echo_off);
	
		switch(true){
	
			case is_null($responses):
			case is_bool($responses):
		
				while( !in_array(strtolower($answer), array('y', 'n')) ){
					if(!$answer && is_bool($responses)) return $responses;
					echo "Please answer [y/n]: ";
					$answer = static::get_answer($echo_off);
				}
				return (strtolower($answer) == 'y') ? true : false;

			case is_string($responses):

				return (!$answer) ? $responses : $answer;

			case is_array($responses):

				$n = (count($responses) -1);

				if(!$answer) $answer = 0;

				while(!in_array($answer, range(0, $n))){
					echo "Please enter 0-$n: ";
					if(!$answer = static::get_answer($echo_off)) $answer = 0;
				}

				$a = array_keys($responses);
				
				return $a[$answer];

			case is_callable($responses):

				$success = $responses($answer, $question);

				while(!$success){
					echo "$question: ";
					$answer = static::get_answer($echo_off);
					$success = $responses($answer, $question);
				}
				return $answer;
		}
	}

	/*! fetch the user's response
 		\param echo_off whether to echo the user's typing
	*/

	function get_answer($echo_off=false){
		if($echo_off) system('stty -echo');
		$answer = trim(fgets(STDIN)); 
		if($echo_off){
			system('stty echo');
			echo "\n";
		}
		return $answer;
	}

	public function format_print_dump($var){
		var_dump($var);
	}

	public function format_print_json($var){
		echo \lfy\json_encode($var)."\n";
	}

	public function format_print_json_each($var){
		if(!is_scalar($var)) $var = array($var);
		foreach($var as $member) echo \lfy\json_encode($member)."\n";
	}

	public function format_print($var){
		foreach( (array) $var as $member){
			$a = array();
			foreach( (array) $member as $word) $a[] = (string) $word;
			echo implode("\t", $a)."\n";
		}
	}

}

